/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */

package cn.hermit.util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.URLDecoder;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * String utility
 *
 * @author Jack Gao (Chinese name : GAO JIANGUO, Email : linux.gjg@gmail.com)
 * @date 18 Jun, 2014
 *
 * @author JackGao 高建国
 * @description //TODO 设计说明
 * @date 31/5/2017
 */
public class StringUtils {

    public static boolean isBlank(String str) {
        return str == null || str.trim().equals("");
    }

    /**
     * Check if a string is empty or null after being trimmed
     * @param str
     * 		The string to check
     * @return
     */
    public static boolean isEmpty(String str) {
        return isEmpty(str, true);
    }

    /**
     * Check if a string is empty or null
     * @param str
     * 		The string to check
     * @param trimed
     * 		If true, will trim the string before checking
     * @return
     */
    public static boolean isEmpty(String str, boolean trimed) {
        return isEmpty(str, trimed, false);
    }

    /**
     * Check if a string is empty or null
     *
     * @param str
     * 		The string to check
     * @param trimed
     * 		If true, will trim the string before checking
     * @param trimInvisible
     * 		If true, will weed out invisible characters before checking
     * @return
     */
    public static boolean isEmpty(String str, boolean trimed,
                                  boolean trimInvisible) {
        if (str == null || str.length() == 0) {
            return true;
        }
        if (trimed) {
            str = str.trim();
            return "".equals(str);
        }
        if (trimInvisible) {
            for (int i = 0; i < str.length(); i++) {
                char c = str.charAt(i);
                if (!('\n' == c || '\r' == c || '\t' == c || '\b' == c || '\f' == c)) {
                    return false;
                }
            }
            return true;
        }
        return "".equals(str);
    }

    /**
     * Check if all strings are empty or null after all being trimmed
     *
     * @param strs
     * 		The strings to check
     * @return
     */
    public static boolean isAllEmpty(String... strs) {
        return isAllEmpty(true, strs);
    }

    /**
     * Check if all strings are empty or null.
     * @param trimed
     * 		If true, will trim all strings before checking
     * @param strs
     * 		The strings to check
     * @return
     */
    public static boolean isAllEmpty(boolean trimed, String... strs) {
        return isAllEmpty(trimed, false, strs);
    }

    /**
     * Check if all strings are empty or null.
     *
     * @param trimed
     * 		If true, will trim all strings before checking
     * @param trimInvisible
     * 		If true, will week out invisible characters from all strings before checking
     * @param strs
     * 		The strings to check
     * @return
     */
    public static boolean isAllEmpty(boolean trimed, boolean trimInvisible,
                                     String... strs) {
        if (strs == null || strs.length == 0) {
            return true;
        }
        boolean isAllEmpty = true;
        for (String str : strs) {
            isAllEmpty = isAllEmpty && isEmpty(str, trimed, trimInvisible);
        }
        return isAllEmpty;
    }

    /**
     * Trim string
     *
     * @param str
     * 		The string to trim
     * @param trimInvisible
     * 		If true, will week out all invisible characters in string
     * @return
     * 		The trimmed string
     */
    public static String trim(String str, boolean trimInvisible) {
        if (str == null || str.length() == 0)
            return str;
        if (trimInvisible) {
            StringBuilder s = new StringBuilder();
            for (int i = 0; i < str.length(); i++) {
                char c = str.charAt(i);
                if (s.length() == 0) {
                    if ('\n' == c || '\r' == c || '\t' == c || '\b' == c
                            || '\f' == c) {
                        continue;
                    }
                }
                s.append(c);
            }
            str = s.reverse().toString();
            s = new StringBuilder();
            for (int i = 0; i < str.length(); i++) {
                char c = str.charAt(i);
                if (s.length() == 0) {
                    if ('\n' == c || '\r' == c || '\t' == c || '\b' == c
                            || '\f' == c) {
                        continue;
                    }
                }
                s.append(c);
            }
            return s.reverse().toString();
        } else {
            return str.trim();
        }
    }

    /**
     * Get tabs string by depth
     * @param depth
     * 		the depth of tabs
     * @return
     * 		The tabs string
     */
    public static String tabs(int depth) {
        if (depth == 0)
            return "";
        StringBuilder b = new StringBuilder();
        for (int i = 0; i < depth; i++) {
//			b.append('\t');
            b.append("  ");
        }
        return b.toString();
    }

    /**
     * Compiled pattern of regular expression for digital placeholder of parameter
     */
    private static final Pattern PATTERN_PARAMS = Pattern
            .compile("\\{(\\d+)\\}");

    /**
     * Merge parameter values into message with digital placeholder
     * @param msg
     * 		The message
     * @param params
     * 		The parameters to merge
     * @return
     * 		Merged message with parameters
     */
    public static String mergeParams(String msg, String... params) {
        if (isEmpty(msg) || params == null || params.length == 0) {
            return msg;
        }
        Matcher m = PATTERN_PARAMS.matcher(msg);
        while (m.find()) {
            int index = Integer.parseInt(m.group(1));
            if (index >= params.length) {
                continue;
            }
            String replacer = params[index];
            if (replacer == null) {
                continue;
            }
            msg = msg.replace(m.group(), replacer);
        }
        return msg;
    }

    /**
     * Check if two strings are equivalent.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param ignoreReturnKey
     * 		If true, will ignore the difference of new line break between source and destination in comparison
     * @return
     */
    public static boolean equals(String srcStr, String destStr,
                                 boolean ignoreReturnKey) {
        return diffIndex(srcStr, destStr, ignoreReturnKey, false, false, false) == -1;
    }

    /**
     * Get position in which the two strings are to different.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @return
     *
     */
    public static int diffIndex(String srcStr, String destStr) {
        return diffIndex(srcStr, destStr, false, false, false, false);
    }

    /**
     * Get position in which the two strings are to different.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param ignoreReturnKey
     * 		If true, will ignore the difference of new line break between source and destination in comparison
     * @return
     *
     */
    public static int diffIndex(String srcStr, String destStr,
                                boolean ignoreReturnKey) {
        return diffIndex(srcStr, destStr, ignoreReturnKey, false, false, false);
    }

    /**
     * Check if two strings are equivalent.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param ignoreReturnKey
     * 		If true, will ignore the difference of new line break between source and destination in comparison
     * @param ignoreCase
     * 		If true, will be case insensitive in comparison
     * @return
     */
    public static boolean equals(String srcStr, String destStr,
                                 boolean ignoreReturnKey, boolean ignoreCase) {
        return diffIndex(srcStr, destStr, ignoreReturnKey, ignoreCase, false,
                false) == -1;
    }

    /**
     * Get position in which the two strings are to different.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param ignoreReturnKey
     * 		If true, will ignore the difference of new line break between source and destination in comparison
     * @param ignoreCase
     * 		If true, will be case insensitive in comparison
     * @return
     *
     */
    public static int diffIndex(String srcStr, String destStr,
                                boolean ignoreReturnKey, boolean ignoreCase) {
        return diffIndex(srcStr, destStr, ignoreReturnKey, ignoreCase, false,
                false);
    }

    /**
     * Check if two strings are equivalent.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param ignoreReturnKey
     * 		If true, will ignore the difference of new line break between source and destination in comparison
     * @param ignoreCase
     * 		If true, will be case insensitive in comparison
     * @param needTrim
     * 		If true, will strip the two strings before comparison
     * @return
     */
    public static boolean equals(String srcStr, String destStr,
                                 boolean ignoreReturnKey, boolean ignoreCase, boolean needTrim) {
        return diffIndex(srcStr, destStr, ignoreReturnKey, ignoreCase, false,
                needTrim) == -1;
    }

    /**
     * Get position in which the two strings are to different.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param ignoreReturnKey
     * 		If true, will ignore the difference of new line break between source and destination in comparison
     * @param ignoreCase
     * 		If true, will be case insensitive in comparison
     * @param needTrim
     * 		If true, will strip the two strings before comparison
     * @return
     *
     */
    public static int diffIndex(String srcStr, String destStr,
                                boolean ignoreReturnKey, boolean ignoreCase, boolean needTrim) {
        return diffIndex(srcStr, destStr, ignoreReturnKey, ignoreCase, false,
                needTrim);
    }

    /**
     * Check if two strings are equivalent.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param ignoreReturnKey
     * 		If true, will ignore the difference of new line break between source and destination in comparison
     * @param ignoreCase
     * 		If true, will be case insensitive in comparison
     * @param ignoreNoDisplay
     * 		If true, will week out invisible characters in the two strings before comparison
     * @param needTrim
     * 		If true, will strip the two strings before comparison
     * @return
     */
    public static boolean equals(String srcStr, String destStr,
                                 boolean ignoreReturnKey, boolean ignoreCase,
                                 boolean ignoreNoDisplay, boolean needTrim) {
        return diffIndex(srcStr, destStr, ignoreReturnKey, ignoreCase,
                ignoreNoDisplay, needTrim) == -1;
    }

    /**
     * Get position in which the two strings are to different.
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param ignoreReturnKey
     * 		If true, will ignore the difference of new line break between source and destination in comparison
     * @param ignoreCase
     * 		If true, will be case insensitive in comparison
     * @param ignoreNoDisplay
     * 		If true, will week out invisible characters in the two strings before comparison
     * @param needTrim
     * 		If true, will strip the two strings before comparison
     * @return
     *
     */
    public static int diffIndex(String srcStr, String destStr,
                                boolean ignoreReturnKey, boolean ignoreCase,
                                boolean ignoreNoDisplay, boolean needTrim) {
        needTrim = needTrim || ignoreNoDisplay;
        if (srcStr == destStr)
            return -1;
        if (srcStr == null || destStr == null) {
            return 0;
        }
        if (srcStr.length() == 0 && destStr.length() == 0) {
            return -1;
        }
        int j = 0;
        int i = 0;
        char s = (char) -1;
        char d = (char) -1;
        if (needTrim) {
            while (i < srcStr.length() && (s = srcStr.charAt(i++)) <= ' ')
                ;
            while (j < destStr.length() && (d = destStr.charAt(j++)) <= ' ')
                ;
            if (i == srcStr.length() && j == destStr.length()) {
                return -1;
            }
            if (i == srcStr.length() || j == destStr.length()) {
                return i;
            }
            if (ignoreCase && Character.isUpperCase(s)) {
                s = Character.toLowerCase(s);
            }
            if (ignoreCase && Character.isUpperCase(d)) {
                d = Character.toLowerCase(d);
            }
            if (s != d) {
                return i;
            }
        }
        if (ignoreReturnKey) {
            while (i < srcStr.length() && (s = srcStr.charAt(i++)) == '\r')
                ;
            while (j < destStr.length() && (d = destStr.charAt(j++)) == '\r')
                ;
            if (i == srcStr.length() && j == destStr.length()) {
                return -1;
            }
            if (i == srcStr.length() || j == destStr.length()) {
                return i;
            }
            if (ignoreCase && Character.isUpperCase(s)) {
                s = Character.toLowerCase(s);
            }
            if (ignoreCase && Character.isUpperCase(d)) {
                d = Character.toLowerCase(d);
            }
            if (s != d) {
                return i;
            }
        }

        while (i < srcStr.length() && j < destStr.length()) {
            if (ignoreNoDisplay || ignoreReturnKey) {
                if (ignoreNoDisplay) {
                    while (i < srcStr.length()
                            && (s = srcStr.charAt(i++)) <= ' ')
                        ;
                    while (j < destStr.length()
                            && (d = destStr.charAt(j++)) <= ' ')
                        ;
                    if (i == srcStr.length() && j == destStr.length()) {
                        return -1;
                    }
                    if (i == srcStr.length() || j == destStr.length()) {
                        return i;
                    }
                    if (ignoreCase && Character.isUpperCase(s)) {
                        s = Character.toLowerCase(s);
                    }
                    if (ignoreCase && Character.isUpperCase(d)) {
                        d = Character.toLowerCase(d);
                    }
                    if (s != d) {
                        return i;
                    }
                }
                if (ignoreReturnKey) {
                    while (i < srcStr.length()
                            && (s = srcStr.charAt(i++)) == '\r')
                        ;
                    while (j < destStr.length()
                            && (d = destStr.charAt(j++)) == '\r')
                        ;
                    if (i == srcStr.length() && j == destStr.length()) {
                        return -1;
                    }
                    if (i == srcStr.length() || j == destStr.length()) {
                        return i;
                    }
                    if (ignoreCase && Character.isUpperCase(s)) {
                        s = Character.toLowerCase(s);
                    }
                    if (ignoreCase && Character.isUpperCase(d)) {
                        d = Character.toLowerCase(d);
                    }
                    if (s != d) {
                        return i;
                    }
                }
            } else {
                s = srcStr.charAt(i++);
                d = destStr.charAt(j++);
            }
            if (s == d)
                continue;
            // if ignoreNoDisplay is false, no need to check if needTrim
            if (s <= ' ' && d <= ' ') {
                if (needTrim) {
                    while (i < srcStr.length()
                            && (s = srcStr.charAt(i++)) <= ' ')
                        ;
                    while (j < destStr.length()
                            && (s = destStr.charAt(j++)) <= ' ')
                        ;
                    if (i == srcStr.length() && j == destStr.length()) {
                        return -1;
                    }
                    return i;
                } else {
                    return i;
                }
            }
            if (s <= ' ' || d <= ' ')
                return i;
            if (ignoreCase && Character.isUpperCase(s)) {
                s = Character.toLowerCase(s);
            }
            if (ignoreCase && Character.isUpperCase(d)) {
                d = Character.toLowerCase(d);
            }
            if (s != d)
                return i;
        }
        if (i == srcStr.length() && j == destStr.length()) {
            return -1;
        }
        s = (char) -1;
        d = (char) -1;
        if (needTrim) {
            while (i < srcStr.length() && (s = srcStr.charAt(i++)) <= ' ')
                ;
            while (j < destStr.length() && (d = destStr.charAt(j++)) <= ' ')
                ;
            if (i == srcStr.length() && j == destStr.length()) {
                return -1;
            }
            if (i == srcStr.length() || j == destStr.length()) {
                return i;
            }
            if (ignoreCase && Character.isUpperCase(s)) {
                s = Character.toLowerCase(s);
            }
            if (ignoreCase && Character.isUpperCase(d)) {
                d = Character.toLowerCase(d);
            }
            if (s != d) {
                return i;
            }
        }
        if (ignoreReturnKey) {
            while (i < srcStr.length() && (s = srcStr.charAt(i++)) == '\r')
                ;
            while (j < destStr.length() && (d = destStr.charAt(j++)) == '\r')
                ;
            if (i == srcStr.length() && j == destStr.length()) {
                return -1;
            }
            if (i == srcStr.length() || j == destStr.length()) {
                return i;
            }
            if (ignoreCase && Character.isUpperCase(s)) {
                s = Character.toLowerCase(s);
            }
            if (ignoreCase && Character.isUpperCase(d)) {
                d = Character.toLowerCase(d);
            }
            if (s != d) {
                return i;
            }
        }
        return s == d ? -1 : i;
    }

    /**
     * Get string value of object
     * @param obj
     * 		The object
     * @return
     * 		The string value
     */
    public static String valueOf(Object obj) {
        if (obj == null)
            return "null";
        if (obj.getClass().isArray()) {
            int len = Array.getLength(obj);
            StringBuilder s = new StringBuilder("[");
            for (int i = 0; i < len; i++) {
                Object o = Array.get(obj, i);
                if (o == null) {
                    s.append("null");
                } else {
                    s.append(o.toString());
                }
                if (i != len - 1) {
                    s.append(", ");
                }
            }
            s.append("]");
            return s.toString();
        }
        return obj.toString();
    }

    /**
     * Get UNICODE string value of character
     *
     * @param ch
     * 		The character
     * @return
     * 		The UNICODE string value
     */
    public static String valueOfUnicode(int ch) {
        StringBuilder sb = new StringBuilder("\\u");
        String ss = Integer.toHexString(ch);
        for (int k = 0; k < 4 - ss.length(); k++) {
            sb.append('0');
        }
        sb.append(ss.toUpperCase());
        return sb.toString();
    }

    /**
     * Get Latin string value of character
     *
     * @param ch
     * 		The character
     * @return
     * 		The Latin string
     */
    public static String valueOfLatin(int ch) {
        StringBuilder sb = new StringBuilder("\\x");
        String ss = Integer.toHexString(ch);
        for (int k = 0; k < 2 - ss.length(); k++) {
            sb.append('0');
        }
        sb.append(ss.toUpperCase());
        return sb.toString();
    }

    /**
     * Get string of file content
     * @param file
     * 		The file
     * @return
     * 		String of file content
     * @throws IOException
     */
    public static String valueOf(File file) throws IOException {
        InputStream inStream = null;
        try {
            inStream = new FileInputStream(file);
            return valueOf(inStream);
        } finally {
            if (inStream != null) {
                inStream.close();
            }
        }
    }

    /**
     * Get string of stream content
     *
     * @param inStream
     * 		The stream
     * @return
     * 		String of stream content
     * @throws IOException
     */
    public static String valueOf(InputStream inStream) throws IOException {
        return valueOf(inStream, "UTF-8");
    }

    /**
     * Get string of stream content with encoding
     *
     * @param inStream
     * 		The stream
     * @param encoding
     * 		The encoding of stream
     * @return
     * 		String of stream content with encoding
     * @throws IOException
     */
    public static String valueOf(InputStream inStream, String encoding)
            throws IOException {
        ByteArrayOutputStream outStream = null;
        outStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inStream.read(buffer)) != -1) {
            outStream.write(buffer, 0, len);
        }
        return new String(outStream.toByteArray(), encoding);
    }

    /**
     * Get string value of Throwable's message stack
     *
     * @param e
     * 		The Throwable
     * @return
     * 		String value of Throwable's message stack
     */
    public static String errorMessage(Throwable e) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(baos);
        e.printStackTrace(ps);
        ps.flush();
        return new String(baos.toByteArray());
    }

    /**
     * Get string value of Throwable
     *
     * @param e
     * 		The Throwable
     * @return
     * 		String value of Throwable
     */
    public static String errorSimpleMessage(Throwable e) {
        String errorMsg = e.getMessage();
        if (errorMsg == null || "".equals(errorMsg.trim())) {
            errorMsg = e.getLocalizedMessage();
        }
        if (errorMsg == null || "".equals(errorMsg.trim())) {
            if (e.getCause() != null) {
                errorMsg = errorSimpleMessage(e.getCause());
            }
        }
        if (errorMsg == null || "".equals(errorMsg.trim())) {
            errorMsg = e.getClass().getName();
        }
        return errorMsg;
    }

    /**
     * Match the same parts with minimum length between tow strings.
     *
     * Only used for ASCII code.
     * <p>
     * Matrix algorithm
     *
     * ~ a c d e f g d e <br>
     * c 0 1 0 0 0 0 0 0 <br>
     * c 0 1 0 0 0 0 0 0 <br>
     * d 0 0 1 0 0 0 1 0 <br>
     * e 0 0 0 1 0 0 0 1 <br>
     * f 0 0 0 0 1 0 0 0 <br>
     * d 0 0 1 0 0 0 1 0 <br>
     * e 0 0 0 1 0 0 0 1 <br>
     * h 0 0 0 0 0 0 0 0 <br>
     *
     * </p>
     *
     * @param srcStr
     * 		The source string
     * @param destStr
     * 		The destination string
     * @param len
     * 		The minimum length of same parts
     *            <p>
     *            if the value is less than 2, will return the short string of the two.
     *            </p>
     * @return String[] same parts in two string
     */
    public static String[] match(String srcStr, String destStr, int len,
                                 boolean caseSensitive) {
        if (srcStr == null || destStr == null)
            return null;
        if (len > srcStr.length() || len > destStr.length())
            return null;
        boolean isTheShort = srcStr.length() < destStr.length();
        String lStr = isTheShort ? destStr : srcStr;
        String sStr = isTheShort ? srcStr : destStr;
        if (!caseSensitive) {
            lStr = lStr.toLowerCase();
            sStr = sStr.toLowerCase();
        }
        if (len <= 1) {
            return new String[] { sStr };
        }
        if (len == sStr.length() && lStr.contains(sStr)) {
            return new String[] { sStr };
        }
        Set<String> matched = new HashSet<String>();
        for (int i = 0; i <= lStr.length() - len; i++) {
            int k = 0;
            for (int j = 0; j <= sStr.length(); j++) {
                int li = j + i;
                if (j == sStr.length() || li >= lStr.length()) {
                    if (k >= len) {
                        matched.add(sStr.substring(j - k, j));
                    }
                    break;
                }
                char lCh = lStr.charAt(li);
                char sCh = sStr.charAt(j);
                if (lCh == sCh) {
                    k += 1;
                } else {
                    if (k >= len) {
                        matched.add(sStr.substring(j - k, j));
                    }
                    k = 0;
                }
            }
        }
        for (int i = 1; i <= sStr.length() - len; i++) {
            int k = 0;
            for (int j = 0; j <= sStr.length() - 1; j++) {
                int si = j + i;
                if (j == sStr.length() - 1 || si >= sStr.length()) {
                    if (k >= len) {
                        matched.add(lStr.substring(j - k, j));
                    }
                    break;
                }
                char lCh = lStr.charAt(j);
                char sCh = sStr.charAt(si);
                if (lCh == sCh) {
                    k += 1;
                } else {
                    if (k >= len) {
                        matched.add(lStr.substring(j - k, j));
                    }
                    k = 0;
                }
            }
        }
        return matched.size() == 0 ? null : matched.toArray(new String[] {});
    }

    /**
     * Check if message is encoded with UTF-8
     * @param msg
     * 		The message
     * @return
     */
    public static boolean isEncoded(final String msg) {
        return isEncoded(msg, "UTF-8");
    }

    /**
     * Check if message is encoded with given encoding
     * @param msg
     * 		The message
     * @param encoding
     * 		The given encoding
     * @return
     */
    public static boolean isEncoded(final String msg, String encoding) {
        if (isEmpty(msg)) {
            return true;
        }
        try {
            return !msg.equals(URLDecoder.decode(msg, encoding));
        } catch (final UnsupportedEncodingException e) {
            return false;
        }
    }
}
